package org.flexthinker.binding.ui.form.binding.support
{
    import flash.display.DisplayObject;

    import org.flexthinker.binding.bean.Binding;

    import org.flexthinker.binding.form.FormModel;
    import org.flexthinker.binding.ui.form.binding.BindableUIForm;
    import org.flexthinker.binding.ui.form.binding.Binder;
    import org.flexthinker.binding.ui.form.binding.BinderFactory;
    import org.flexthinker.binding.ui.form.binding.BindingFieldFinder;
    import org.flexthinker.binding.ui.form.binding.BindingFieldFinderListener;
    import org.flexthinker.binding.ui.form.binding.flex.DefaultBindingFieldFinder;
    import org.flexthinker.binding.ui.form.binding.flex.DefaultFlexBinderFactory;

    /**
     * Class extending the functionality of BaseFormBinding, adding support to <code>autoBind</code>.
     * <p>
     *
     * AutoBind tries to match each UI field name with the Binding source field name.
     * Field name must match model property name in order to auto bind them. When there's a match
     * a <code>Binder</code> is created automatically for that field and that UI component.
     *
     * The <code>Binder</code> is created by a <code>BinderFactory</code> implementation. The default implementation
     * of the <code>BinderFactory</code> only emphasis on a limited set of Flex 4 components, but other
     * <code>BinderFactory</code> implementations could be provided.
     * </p>
     *
     * <p>
     *     <code>BindableUIForm</code> interface must be implemented by the form, for performance reasons.
     *     UI Field contained by the form could have been discovered by introspecting the form, but this would
     *     have added complexity to the processing time. Forms could extend other form classes so the introspection
     *     could take too much time. For performance reasons, BindableUIForm has been chosen as the contract needed
     *     to return the specific fields to be exposed for auto binding.
     * </p>
     *
     * @see org.flexthinker.binding.ui.form.binding.BinderFactory
     * @see org.flexthinker.binding.ui.form.binding.flex.DefaultFlexBinderFactory
     */
    public class FormBinding extends BaseFormBinding implements BindingFieldFinderListener
    {
        private var form:DisplayObject;
        private var formModel:FormModel;

        /**
         * Factory that returns the Binder corresponding to a form element.
         */
        private var binderFactory:BinderFactory = new DefaultFlexBinderFactory();

        /**
         * Class used to find the fields in the form, which can be used to auto-bind to FormBinding's source FormModel.
         */
        private var fieldFinder:BindingFieldFinder;

        /**
         * Constructor.
         *
         * @param form
         *          Visual form that contains bindable fields.
         * @param sourceFormModel
         *          Source for binding.
         * @param autoBindFlag
         *          When true, it will try to autoBind instantly.
         *          <code>form</code> and <code>sourceFormModel</code> must be non-null
         *          in order to auto bind. If the condition is not satisfied autoBind flag is ignored.
         *
         * @see org.flexthinker.binding.ui.form.binding.support.FormBinding
         */
        public function FormBinding(form:DisplayObject, sourceFormModel:FormModel = null, autoBindFlag:Boolean = false)
        {
            super(sourceFormModel);

            if (form == null)
            {
                throw new ArgumentError("Can't create a FormBinding out of a null form. Please provide a non-null form");
            }

            this.form = form;
            this.formModel = sourceFormModel;

            if (autoBindFlag && sourceFormModel)
            {
                autoBind();
            }
        }

        public function autoBind():void
        {
            throwIfInvalidFormModel();

            for each (var element:DisplayObject in getBindingFieldFinder().getBindableFields())
            {
                if (element != null)
                {
                    addBinderFor(element);
                }
            }
        }

        public function getForm():DisplayObject
        {
            return form;
        }


        public function fieldAdded(fieldName:String, instance:Object):void
        {
            addBinderFor(DisplayObject(instance));
        }

        public function fieldRemoved(fieldName:String, instance:Object):void
        {
            removeBinderFor( DisplayObject(instance));
        }

        // ---------------------
        //    internal methods
        // ---------------------

        /**
         * Factory used to create <code>Binders</code>.
         * @return
         *    <code>BinderFactory</code> that returns the <code>Binder</code> corresponding to a form element.
         *
         * @see org.flexthinker.binding.ui.form.binding.Binder
         * @see org.flexthinker.binding.ui.form.binding.flex.DefaultFlexBinderFactory
         */
        protected function getBinderFactory():BinderFactory
        {
            return binderFactory;
        }

        /**
         * Class used to find the fields in the form, which can be used for auto-bind.
         * @return
         *      <code>BindingFieldFinder</code> implementation. Default implementation is <code>DefaultBindingFieldFinder</code>.
         *
         * @see org.flexthinker.binding.ui.form.binding.flex.DefaultBindingFieldFinder
         */
        protected function getBindingFieldFinder():BindingFieldFinder
        {
            if (!fieldFinder)
            {
                fieldFinder = new DefaultBindingFieldFinder(BindableUIForm(getForm()));
                fieldFinder.addFinderListener(this);
            }
            return fieldFinder;
        }

        private function getElementId(object:DisplayObject):String
        {
            if (object && object.hasOwnProperty("id") && object["id"])
            {
                return object["id"].toString();
            }
            return object.name;
        }

        private function formModelHasProperty(formProperty:String):Boolean
        {
            return getFormModel() != null && getFormModel().getFormObject() != null &&
                    getFormModel().getFormObject().hasOwnProperty(formProperty);
        }

        private function addBinderFor(element:DisplayObject):void
        {
            var elementId:String = getElementId(element);

            if (formModelHasProperty(elementId) && getFieldBinder(elementId) == null)
            {
                var binder:Binder = getBinderFactory().getBinder(element, elementId);
                if (binder)
                {
                    addFieldBinder(binder);
                }
            }
        }

        private function removeBinderFor(element:DisplayObject):void
        {
            var elementId:String = getElementId(element);
            var binder:Binder = getFieldBinder(elementId);
            if ( binder )
            {
                removeFieldBinder(binder);
            }
        }
    }
}
